\documentclass{manual}

\title{Eurasia 用户指引}

\author{沈崴}

\authoraddress{
	项目: \url{http://eurasia.pyforce.com}
	邮件列表: \url{https://groups.google.com/group/eurasia-users}
}

\date{2009 年 3 月 23 日}

\release{3.0.0b}

\makeindex

\begin{document}

\maketitle

\begin{abstract}

\noindent

Eurasia 高性能应用服务器和开发框架, 可以用于面向长连接和高并发的 tcp/http 服务器设计。
自带 shelve2 对象数据库、编译型模板、fastcgi 及 ssl/https 支持, 也可以作为 wsgi 服务器。

\end{abstract}

\tableofcontents

\chapter{安装}

\section{系统要求}

eurasia 基于 python2.5。一般来说, 支持 python2.5 的计算机平台都能够运行 eurasia。

推荐配置:

\begin{itemize}

\item \strong{32 位 x86 体系结构}

目前尚没有证据证明 eurasia 能通过 64 位或其他体系结构来获得额外的性能提升。

\item \strong{unix/linux 操作系统}

eurasia 基于 unix 异步 IO 设计。

\item \strong{Stackless Python 2.5.2}

eurasia 基于 stackless python 协程技术 (轻便线程、微线程) 设计。所以 stackless python 是最适合的。

\end{itemize}

\section{兼容性话题}

\strong{windows 操作系统}

需要注意的是, eurasia 在 windows 上不能达到 eurasia 在 unix/linux 上的并发性能。

\strong{python 2.5}

普通的 python 2.5 可以通过 greenlet 技术来实现 eurasia 所需的 stackless 特性。在性能上并不会有明显差别。
如果你正在使用非 stackless 的 python 版本, eurasia 会自动从网上下载并安装 py 包, 你需要准备好网络连接。
因为索引会比较耗时, 这里推荐先对 py 包进行手工安装 (stackless python 不需要), 再安装 eurasia。

\strong{python 2.6}

Python 2.6 是否能够很好地兼容 eurasia, 未经充分测试。

\strong{python 3}

eurasia 目前不支持 python 3。

\section{开始安装}

Eurasia 的官方网站是 \url{http://code.google.com/p/eurasia}
可以在这里下载到 eurasia 的最新版本。

你可以通过解压目录中的安装脚本 \file{setup.py} 来安装 eurasia 库:

\begin{verbatim}
    python setup.py install
\end{verbatim}

对于非 stackless 的 python 2.5, eurasia 需要 py (\url{http://codespeak.net/py}) 包的支持。
如果没有检查到 py 包, 安装程序会尝试通过 setuptools 自动下载并安装 py 包。

\note{因为 py 包在自动安装时, 索引会比较耗时, 所以强烈推荐在安装 eurasia 之前手工下载 py 包并进行安装。
具体请访问 py 项目网站 http://codespeak.net/py 。}

\note{如果需要用到 ssl/https 支持, 你需要安装有 pyOpenSSL, eurasia 并不会自动安装 pyOpenSSL,
请访问 pyOpenSSL 项目网站 http://pyopenssl.sourceforge.net/ 。}

\chapter{教程}

\section{快速开始}

我们将从最简单的 hello world 开始, 通过一些范例程序, 快速进入 eurasia 的世界。

\subsection{hello world!}

创建一个名为 \file{hello.py} 的文件。

\begin{verbatim}
#!/usr/bin/python2.5
from eurasia import config, mainloop
def handler(httpfile):
	httpfile['Content-Type'] = 'text/html'
	httpfile.write('<html>hello world!</html>')
	httpfile.close()

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

执行该脚本, 启动服务器。

\begin{verbatim}
$/usr/bin/python hello.py
\end{verbatim}

这个程序简单地把处理函数 handler 绑定到本机的 8080 端口,
所有发往本机 8080 端口的 HTTP 请求都会得到一个 "hello world!" 的响应。

这样, 我们的第一个 eurasia 程序就完成了。而且这的确已经包含了 eurasia web 框架的大部分接口。

\subsection{httpfile 对象}

在上面的例子中, 我们已经接触到 httpfile 对象。
httpfile 对象封装了我们进行 web 开发要用到的所有功能。

httpfile 对象包含以下读相关的属性。

\begin{tableii}{c|l}{}{ 属性 }{ 信息 }
\lineii{ httpfile.uri            }{ 完整的请求地址, 比如 "/show?page=10" }
\lineii{ httpfile.method         }{ 请求方法, "GET" 或 "POST" }
\lineii{ httpfile.version        }{ "HTTP/1.0" 或 "HTTP/1.1" }
\lineii{ httpfile[key]           }{ 即 __getitem__ 方法。用于获取 http 头, 比如 httpfile["User-Agent"] }
\lineii{ httpfile.get(key, default=None) }{ 用于获取 http 头 }
\lineii{ httpfile.items() }{ 以键值对 key value 的形式获取所有 http 头 }
\lineii{ httpfile.keys() }{ 获取所有 http 头的键 }
\lineii{ httpfile.values() }{ 获取所有 http 头的值 }
\lineii{ httpfile.environ        }{ 类似于 cgi 的环境变量 }
\lineii{ httpfile.path_info      }{ 参见 cgi1.1 协议关于 PATH_INFO 的说明, 如果 uri 是 "/show?page=10" 那么 path_info 为 "/show" }
\lineii{ httpfile.script_name    }{ 参见 cgi1.1 协议关于 SCRIPT_NAME 的说明, 通常情况下仅对 fcgi 有效, 对应用服务器无效 }
\lineii{ httpfile.query_string   }{ uri 的请求部分, 如果 uri 是 "/show?page=10" 那么 query_string 为 "page=10" }
\lineii{ httpfile.address        }{ 客户端的 ip 与端口, 诸如 ('192.168.0.101', 4433) }
\lineii{ httpfile.read(size)     }{ 对 "POST" 请求读取指定大小的请求报文, 如果不指定 size 则全部读取 }
\lineii{ httpfile.readline(size) }{ 读取一行或指定大小的报文, 如果不指定 size 则到完成读取一行为止 (直至文件结尾) }
\end{tableii}

httpfile 对象包含以下写相关的属性。

\begin{tableii}{c|l}{}{ 属性 }{ 信息 }
\lineii{ httpfile[key]        }{ 即 __setitem__ 方法。用于设置响应的 http 头, 比如 httpfile['Content-Type']='text/html'}
\lineii{ httpfile.update(*args, **kwargs) }{ 使用类似 dict.update 的方式设置响应的 http 头 }
\lineii{ httpfile.setdefault(key, value) }{ 如果 key 指定的响应头没有设置, 设置为 value }
\lineii{ httpfile.status      }{ 设置响应状态, 正常响应是 200 (200 是默认值, 常见的还有 404 "未找到" 等) }
\lineii{ httpfile.write(data) }{ 发送文本 data 到客户端 }
\lineii{ httpfile.close()     }{ 完成本次请求 }
\end{tableii}

\note{httpfile 对象具有 __getitem__ 和 __setitem__ 方法, 可以像字典那样读写。
但是读取的是请求的头部, 写的是响应的头部, 所以写进去和读出来的值并不一致, 这是需要注意的。}

\subsection{一个综合应用}

下面运用 httpfile 对象编写一个简单的文件服务器。

\note{本范例仅用于演示。}

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-

import os.path
from eurasia import config, mainloop

def handler(httpfile):
	filename = '/var/www' + httpfile.path_info
	if not os.path.exists(filename): # 文件不存在  , 返回 404 (未找到)
		httpfile.status = 404
		httpfile.write('<h1>Not Found</h1>')
		return httpfile.close()

	if os.path.isdir(filename):     # 请求的是目录, 返回 403 (拒绝访问)
		httpfile.status = 403
		httpfile.write('<h1>Forbidden</h1>')
		return httpfile.close()

	try:
		data = open(filename).read()
	except (IOError, OSError):      # 文件读取错误, 返回 500
		httpfile.status = 500
		httpfile.write('<h1>Internal Server Error</h1>')
		return httpfile.close()

	# 正确返回文件内容, 因为设定了 Content-Type 为 application/octet-stream,
	# 浏览器将提示用户是否下载该文件, 而不是直接显示出来
	httpfile['Content-Type'] = 'application/octet-stream'
	httpfile.write(data)
	httpfile.close()

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

\section{服务器配置}

我们不仅可以启动一个服务器, 还可以配置出多个服务器:

\begin{verbatim}
config(handler=handler1, port=8080)
config(handler=handler2, port=9080)
mainloop()
\end{verbatim}

除了把服务器配置到端口上, 我们还可以详细指定绑定的端口。
而且服务器不仅可以绑定到一个 ip 和端口上, 还可以绑定到多个:

\begin{verbatim}
config(handler=handler, bind='192.168.0.1:8080, 192.168.0.2:80')
\end{verbatim}

如果既不指定 "port" 也不指定 "bind", 那么服务器将会以 fastcgi 模式启动,
下面是一个完整的 fcgi 范例:

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-

# 这是一个 fastcgi 程序
# 带有 mod_fcgid 或 mod_fastcgi 的 apache 可以直接启动该程序

from eurasia import config, mainloop
def handler(httpfile):
	httpfile.write('<html>hello world!</html>')
	httpfile.close()

config(handler=handler)
mainloop()
\end{verbatim}

下面是关于服务器配置更为详细的说明。

\subsection{FCGI 配置}

如果既不指定 "bind" 也不指定 "port", 那么服务器将会以 fastcgi 模式启动:

\begin{verbatim}
config(handler=handler)
\end{verbatim}

如果你正在使用带有 mod_fcgid 或者 mod_fastcgi 的 apache,
那么将可以直接启动该服务器程序。

对于 lighttpd 和 nginx 可以使用 lighttpd 项目的 spawn-fcgi 程序。

eurasia 同时支持以外部服务器模式启动 fcgi 服务器,
这时候你需要通过 "bind" 或 "port" 指定地址,
并设定 "fcgi" 参数为 True:

\begin{verbatim}
config(handler=handler, bind="127.0.0.1:8080", fcgi=True)
\end{verbatim}

\subsection{绑定到 IPv6 及 Unix Socket}

eurasia 支持绑定到 IPv6, 你需要将 IPv6 地址放在 "[xx:xx:...:xx]" 中:

\begin{verbatim}
config(handler=handler, bind="[::]:8080")
\end{verbatim}

eurasia 支持绑定到 Unix Socket, 你仅需给出 Unix Socket 域文件路径:

\begin{verbatim}
config(handler=handler, bind="/var/example/test.sock")
\end{verbatim}

除了字符串参数外 "bind", 还可以接受一个地址列表:

\begin{verbatim}
config(handler=handler, bind=[
	('0.0.0.0', 8080), # IPv4 地址, 绑定到 8080 端口
	('::'     , 9080), # IPv6 地址, 绑定到 9080 端口
	('/var/example/test.sock') # Unix Socket
	])
\end{verbatim}

等价于:

\begin{verbatim}
config(handler=handler, bind='0.0.0.0:8080, [::]:9080, /var/example/test.sock')
\end{verbatim}

对 Unix Socket 的配置而言,
使用地址列表比起使用字符串参数的优势是可以指定各种字符及编码的路径和文件名。

\subsection{使用 SSL/HTTPS}

让服务器支持 ssl 和 https, 你只需要指定 ssl private key 和 ssl certificate 文件即可:

\begin{verbatim}
config(handler=handler, bind='0.0.0.0:443',
	ssl_private_key='/PATH/TO/server.key', # private key 文件
	ssl_certificate='/PATH/TO/server.crt'  # certificate 文件
	)
\end{verbatim}

unix/linux 用户可以方便地通过 openssl 命令生成 private key 和 certificate 文件。

\note{ssl/https 支持依赖于 pyOpenSSL 包, eurasia 并不会自动安装 pyOpenSSL,
请访问 pyOpenSSL 项目网站 http://pyopenssl.sourceforge.net/ 。}

\subsection{其他配置}

我们还可以指定调试模式、用户名等参数。

\begin{verbatim}
import eurasia
eurasia.verbose =  False   # 禁止输出调试信息, 默认为 True
eurasia.user    = 'nobody' # 以 'nobody' 的身份执行程序
\end{verbatim}

\section{表单、文件上传及 Session 处理}

你可以在 eurasia 里方便地处理表单和文件上传。

表单及文件上传处理功能是由 eurasia.cgietc 模块提供的。

\begin{verbatim}
from eurasia.cgietc import Form, SimpleUpload
\end{verbatim}

另外从 eurasia.web 和 eurasia.fcgi 导入也是一样的。

\begin{verbatim}
from eurasia.web  import Form, SimpleUpload
from eurasia.fcgi import Form, SimpleUpload
\end{verbatim}

\note{cgietc 模块包括 Form, SimpleUpload、Browser、json、wsgi 这几个实用接口。}

\subsection{Form}

Form 函数将用户提交的表单解析成一个 dict 对象。

\begin{verbatim}
formdict = Form(httpfile)
\end{verbatim}

你可以通过设定 max_size 来限制用户 POST 数据的大小, 如果用户提交的数据超过限制, Form 函数会抛出 IOError。

\begin{verbatim}
#!/usr/bin/python2.5
import os.path
from eurasia.cgietc import Form
from eurasia import config, mainloop

def handler(httpfile):
	try:
		form = Form(httpfile, max_size=1024)
	except IOError:
		httpfile.status = 500
		httpfile.close()

	httpfile['Content-Type'] = 'text/plain'
	print >> httpfile, `form`
	httpfile.close()

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

\note{如果用户提交了多个重名的表单变量, Form 会使用 list 来保存。比如 "?a=1\&a=2\&a=3\&b=4", form['a'] 的取值是 ['1', '2', '3'], 而 form['b'] 的取值是 '4'。}

\subsection{文件上传}

这是一个典型的文件提交页面。

\begin{verbatim}
<html>
<form action="/simpleupload" method="post" enctype="multipart/form-data">
<input type="hidden" name="a" value="表单变量A" />
<input type="file" name="z-file" />
<input type="submit" />
</form>
</html>
\end{verbatim}

eurasia 的 SimpleUpload 工具对文件上传提供了支持。

\begin{verbatim}
fd = SimpleUpload(httpfile)
\end{verbatim}

SimpleUpload 会返回一个上传文件的文件描述符 (fd), 我们可以从 fd.filename 和 fd.size 中取出文件名和文件大小。
其余的表单变量可以通过 fd[key] 取出。

\begin{tableii}{c|l}{}{ 属性 }{ 信息 }
\lineii{ SimpleUpload.size           }{ 上传文件的大小 }
\lineii{ SimpleUpload.filename       }{ 上传文件的文件名 }
\lineii{ SimpleUpload[key]           }{ 除了上传文件之外, 其余表单变量 }
\lineii{ SimpleUpload.read(size)     }{ 从上传文件中读取指定大小的内容, 默认读取全部内容 }
\lineii{ SimpleUpload.readline(size) }{ 行读取 }
\end{tableii}

下面是完整的处理脚本。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia import config, mainloop
from eurasia.cgietc import SimpleUpload

page = '''\
<html>
<form action="/simpleupload" method="post" enctype="multipart/form-data">
<input type="hidden" name="a" value="表单变量A" />
<input type="file" name="z-file" />
<input type="submit" />
</form>
</html>
'''

def handler(httpfile):
	if httpfile.path_info != '/simpleupload':
		httpfile.write(page) # 输出前面定义的上传表单
		return httpfile.close()

	fd = SimpleUpload(httpfile)
	a = fd['a'] # <input type="hidden" name="a" value="..."/>

	filename = fd.filename
	size     = fd.size
	data     = fd.read()  # 读取所有内容

	httpfile['Content-Type'] = 'text/plain; charset=utf-8'

	print >> httpfile, ''
	print >> httpfile, '变量 a:', a
	print >> httpfile, '文件名:', filename
	print >> httpfile, '文件预测大小:', size
	print >> httpfile, '文件实际大小:', len(data)

	httpfile.close()

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

\note{SimpleUpload 要求表单变量名 (这里是 'a') 必须小于文件控件的名称 (这里是 'z-file')。
因为对 SimpleUpload 组件来说, 文件组件的名字并不重要, 所以请尽量起成 "zzzzzzzz" 之类的名字,
以保证他的值是最大的。另外, SimpleUpload 组件只支持上传一个文件。
当需要提交多个文件时, 你可以使用多个 SimpleUpload 组件。}

\note{SimpleUpload 并不会真的把用户上传的文件建立在磁盘上, 也不会把用户的上传读取到内存中,
而是在调用 read() 时才会从 socket 中去获取。因此, 在没有读取完整个上传文件之前,
上传用户的浏览器将一直保持在 "正在上传" 的状态下。}

\note{http 协议并不是每次都确切地知道用户上传文件的大小,
SimpleUpload 能够猜测出用户上传内容的大小, 对于标准的浏览器和客户端这通常是准确的。
在极少数情况下 SimpleUpload.size 和 SimpleUpload.read 取出的内容大小并不匹配,
这时应以 SimpleUpload.read 取出的实际内容大小为准, 一般来说, 你可以认为这是一种恶意行为。}

\note{在向用户发送任何内容之前, 请先确保已经完全读取了上传文件, 否则会发生读写错误 (IOError)。}

\note{在读取文件时如果用户已经断开连接, read 会抛出 IOError。}

\subsection{手工获取 POST 报文}

你可以通过 Form 和 SimpleUpload 读取用户通过 post 方法提交的表单和文件。

如果用户通过 post 方法提交了一个 xml 报文或者 json 等类型的报文,
那么你就需要手工读取整个 post 报文进行处理了。

httpfile.read()/httpfile.readline() 即是这样的工具,
下面是一个 echo 服务器, 将用户 post 过来的数据重新返回:

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
import os.path
from eurasia import config, mainloop

def echo(httpfile):
	# 原样返回用户提交的 post 报文
	if httpfile.method == 'POST':
		httpfile['Content-Type'] = 'text/plain'
		httpfile.write(httpfile.read())
		return httpfile.close()

	# 不支持 GET 方法
	httpfile.status = 405 # Method Not Allowed
	httpfile.close()

config(handler=echo, port=8080)
mainloop()
\end{verbatim}

\note{在向用户发送任何内容之前, 请先确保已经完全读取了 post 报文, 否则会发生读写错误 (IOError)。}

\note{Form 和 SimpleUpload 即是基于 httpfile.read 实现的,
Form、SimpleUpload 和 httpfile.read 会同时读取 post 报文, 他们是不能混用的。}

\subsection{Cookie \& Session}

你可以通过 httpfile['Set-Cookie'] 和 httpfile['Cookie'] 这两个接口直接操作 cookie, 去实现一个自己喜欢的 session 机制。

此外, eurasia 也提供了 session 支持。通过设置 httpfile.uid, 你可以为用户的浏览器指定一个持久 id (基于 cookie), 并通过 httpfile.uid 取出。

\begin{tableii}{c|l}{}{ 属性 }{ 信息 }
\lineii{ httpfile.uid      }{ 取得持久 id }
\lineii{ httpfile.uid = id }{ 设置持久 id }
\end{tableii}

这里是一个简单的演示程序。创建并获取一个 uid。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-

import random, sha
from eurasia.web import config, mainloop

def handler(httpfile):
	# 如果没有 uid 就为该浏览器用户分配一个
	if not httpfile.uid:
		uid = sha.new('%s' %random.random()).hexdigest()
		httpfile.uid = uid
		print >> httpfile, 'uid %r created' %uid
		return httpfile.close()

	# 有 uid 就在页面上输出
	print >> httpfile, 'uid is %r' %httpfile.uid
	httpfile.close()

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

基于持久 id 可以做出各种 session 实现。这里是一个内存 session,
我们将用户提交的表单保存在字典中, 在下一次访问时取出。
使用 dbm、gdbm、bsddb、memcached 以及 MySQL 的原理相同。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-

import random, sha
from eurasia.cgietc import Form
from eurasia import config, mainloop

session = {}
newid = lambda: sha.new('%s' %random.random()).hexdigest()

def handler(httpfile):
	if not httpfile.uid:          # 新建 session
		uid = newid()
		while uid in session:
			uid = newid()

		session[uid] = None
		httpfile.uid = uid
	else:
		uid = request.uid

	data = session[uid]          # 前一次的表单数据
	session[uid] = Form(request) # 在 session 中保存本次的表单数据

	print >> httpfile, data      # 返回前一次的表单数据
	httpfile.close()

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

\section{长连接}

首先只要请求头部指定了 "Connection: keep-alive", eurasia 在请求结束后不会立即断开连接,
而是始终和客户端保持连接。这对开发者是透明的, 你不必关心这一点或者手工维持连接。

除此之外 eurasia 还支持发送长连接响应。

\subsection{httpfile 长连接响应模式}

在通常情况下, httpfile.write() 的内容会在调用 httpfile.close() 时一次性地发送给浏览器客户端。
通过 httpfile.begin() 调用, 我们可以使 httpfile.write() 的内容即时发送给客户端。

在下面这个例子中, 每当有新用户访问站点, 所有在线用户都将收到一条提醒:

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-

from time import strftime
from eurasia import config, mainloop

# 保存了所有在线用户的全局列表
global_httpfiles = set()

def handler(current_httpfile):
	# 响应头部必须在调用 httpfile.begin() 之前设置好
	current_httpfile['Content-Type'] = 'text/html; charset=utf-8'

	# 进入长连接响应模式
	current_httpfile.begin()
	current_httpfile.write(strftime('[%a, %d-%b-%Y %H:%M:%S GMT] 我加入啦!<br/>'))

	# 通知其他在线用户, 有新人加入
	disconnected = []
	for httpfile in global_httpfiles:
		try:
			# 告诉其他在线用户有新人加入
			httpfile.write(strftime('[%a, %d-%b-%Y %H:%M:%S GMT] 又有新人加入啦!<br/>'))
		except IOError:
			# 连接已断开
			disconnected.append(httpfile)

	# 移除已经断开连接的 httpfile
	for httpfile in disconnected:
		global_httpfiles.remove(httpfile)

	# 将当前浏览器添加到全局在线用户列表中
	global_httpfiles.add(current_httpfile)

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

一旦调用 httpfile.begin() 以后, httpfile.write() 会直接输出到浏览器,
httpfile.write() 函数会一直处于 "阻塞" 状态, 直至所有内容发送完毕。

如果在 httpfile.begin() 和 httpfile.write() 调用时与客户端的连接已经断开,
你会得到一个 IOError 异常。

同样使用 httpfile.close() 结束本次请求。

\note{需要注意的是在这个例子中, 因为 firefox 浏览器的缓存策略,
无法同时打开两个 html 长连接页面
(但是下面将要提到的 javascript 远程调用不受影响), 这不是 eurasia 引起的。
如果你正在使用 firefox , 除了 firefox 之外你还要多打开一个浏览器才能看到效果。
或者使用 telnet 发送 "GET / HTTP/1.0" 来进行测试。}

\note{响应头部和 httpfile.status 必须在调用 httpfile.begin() 之前设置好。}

\subsection{javascript rpc}

eurasia.cgietc 提供的 Browser 对象是一种更为强大的长连接工具。
服务器端可以在任何时候即时调用客户端的 javascript 函数 (javascript 远程调用)。

在下面这个例子中, 每当有新用户访问站点,所有在线用户都将收到一条 javascript 的提醒。

我们首先定义一个包含有 javascript 的 html 页面, 并与服务器建立一条长连接。
其中的 "my_alert" 函数就是我们即将要用到的:

\begin{verbatim}
<html>
<head>
<script language="JavaScript">
function my_alert(stuff) { alert(stuff); };
</script>
</head>
<body>
<!-- 与 /remotecall 位置的服务器脚本建立长连接 -->
<iframe src="/remotecall" style="display: none;"></iframe>
</body
</html>
\end{verbatim}

下面是完整的处理脚本:

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-

from eurasia.cgietc import Browser
from eurasia import config, mainloop

page = '''\
<html>
<head>
<script language="JavaScript">
function my_alert(stuff) { alert(stuff); };
</script>
</head>
<body>
<!-- 与 /remotecall 位置的服务器脚本建立长连接 -->
<iframe src="/remotecall" style="display: none;"></iframe>
</body
</html>
'''

# 保存了所有在线用户的全局列表
global_browsers = set()

def handler(httpfile):
	if httpfile.path_info != '/remotecall':
		httpfile.write(page) # 输出前面定义的上传表单
		return httpfile.close()

	# 创建 Browser 对象
	# 调用客户端名为 "my_alert()" 的 js 函数
	current_browser = Browser(httpfile)
	current_browser.my_alert(u'我加入啦!')

	# 通知其他在线用户, 有新人加入
	disconnected = []
	for browser in global_browsers:
		try:
			# 调用在线用户的 my_alert 函数
			browser.my_alert(u'又有新人加入啦!')

		except IOError: # 已经断开连接
			disconnected.append(browser)

	# 移除已经断开连接的浏览器
	for browser in disconnected:
		disconnected.remove(browser)

	# 将当前浏览器添加到全局浏览器对象列表中
	global_browsers.add(current_browser)

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

我们可以在 Brwoser 对象创建时指定 javascript 域 (document.domain),
方便 javascript 的跨子域调用:

\begin{verbatim}
browser = Browser(httpfile, domain=DOMAIN)
\end{verbatim}

\note{如果不指定 domain, Browser 对象会指定 domain 为顶级域名,
如果当前域为 "www.example.com" 那么默认即为 "example.com"}

\section{WSGI}

eurasia 带有 wsgi 服务器, eurasia.WSGIServer 与 flup.WSGIServer 的用法差不多。

\begin{verbatim}
#!/usr/bin/python2.5
from eurasia import WSGIServer
def app(environ, start_response):
	start_response('200 OK', [('Content-Type', 'text/html')])
	return ['<html>hello world!</html>']

server = WSGIServer(app, bindAddress=('0.0.0.0', 8080))
server.run()
\end{verbatim}

\note{除了 bindAddress, 还可以使用 port 和  bind 来指定地址, 如果不指定地址, wsgi 服务器将启动为 fcgi}

你也可以使用 config 函数来配置 wsgi 服务器。

\begin{verbatim}
#!/usr/bin/python2.5
from eurasia import config, mainloop
def app(environ, start_response):
	start_response('200 OK', [('Content-Type', 'text/html')])
	return ['<html>hello world!</html>']

config(app=app, port=8080)
mainloop()
\end{verbatim}

\note{在 wsgi application 中, 我们仍然可以从 environ['wsgi.input'] 中得到 httpfile 对象}

\subsection{启动 django}

首先我们通过 django-admin.py startproject SITE 命令创建一个站点。

我们可以编写如下服务器启动脚本, 使 django 架设在 eurasia 上。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
# django 服务器启动脚本

import sys, os
sys.path.insert(0, '/PATH/TO/DJANGO/PROJECTS')
os.environ['DJANGO_SETTINGS_MODULE'] = 'SITE.settings'

from eurasia import WSGIServer
from django.core.handlers.wsgi import WSGIHandler
server = WSGIServer(wsgiHandler(), port=8080)
server.run()
\end{verbatim}

\note{此种模式执行 django 会有一定限制, 欲完全兼容 django 请看下面 "全栈支持 django 一节"}

\subsection{启动 cherrypy}

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
# cherrypy 服务器启动脚本

import cherrypy
from eurasia import WSGIServer

class HelloWorld:
	def index(self):
		return 'hello world!'

	index.exposed = True

app = cherrypy.tree.mount(HelloWorld())

server = WSGIServer(app) # 启动为 fcgi
server.run()
\end{verbatim}

\subsection{混用多个框架}

eurasia.cgietc 提供的 wsgi 函数可以将任意 WSGI application 转换成普通的 handler。

handler 可以用于 config 函数:

\begin{verbatim}
from eurasia.cgietc import wsgi
config(handler=wsgi(app), port=8080)
\end{verbatim}

既然 handler 本质上是一个接受一个 httpfile 参数的处理函数, 也可以用于多框架混用。

在这个例子中, 我们将混合使用 django 和 cherrypy (以及 eurasia):

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia.cgietc import wsgi
from eurasia import config, mainloop

# django handler

import sys, os
sys.path.insert(0, '/PATH/TO/DJANGO/PROJECTS')
os.environ['DJANGO_SETTINGS_MODULE'] = 'SITE.settings'
from django.core.handlers.wsgi import WSGIHandler

django_app = WSGIHandler()
django_handler = wsgi(django_app)

# cherrypy handler

import cherrypy
class HelloWorld:
	def index(self):
		return 'hello cherrypy!'

	index.exposed = True

cherrypy_app = cherrypy.tree.mount(HelloWorld())
cherrypy_handler = wsgi(cherrypy_app)

# eurasia + django + cherrypy

def handler(httpfile):
	path_info = httpfile.path_info
	if   path_info[:8 ] == '/django/':
		# 去掉 uri 前面 '/django' 部分, 用新地址作为根路径调用 django
		httpfile.path_info = path_info[7:]
		return django_handler(httpfile)

	elif path_info[:10] == '/cherrypy/':
		# 去掉 uri 前面 '/cherrypy' 部分, 用新地址作为根路径调用 cherrypy
		httpfile.path_info = path_info[9:]
		return cherrypy_handler(httpfile)

	else:
		httpfile.write('this is eurasia!')
		return httpfile.close()

config(handler=handler, port=8080)
mainloop()
\end{verbatim}

\note{修改 httpfile 对象的 path_info、script_name、query_string 三个属性,
会重新生成 request_uri 属性。他们也会同时影响到 httpfile.environ 中的
REQUEST_URI、PATH_INFO、SCRIPT_NAME、QUERY_STRING 这四个环境变量。
httpfile.environ 即是 WSGI 环境变量。httpfile.uri 不受他们影响, 不发生变化。}

\note{更深层次的混用, 因为 httpfile 对象被设置在 WSGI 环境中 (envrion['wsgi.input']),
你可以在任何支持 WSGI 的框架中取出 httpfile 对象。
有了 httpfile 就有了 Form、SimpleUpload、Browser ... eurasia 的大部分功能,
这意味着你可以在那些框架中直接获得 eurasia 的功能。
至于如何从 WSGI 环境中得到 wsgi.input, 请参考各框架的具体实现。}

\note{在其他框架中取出 httpfile, 并调用了诸如 httpfile.begin() 之类的长连接接口,
在单个 wsgi application 请求函数结束以后, httpfile 不会自动关闭,
需要显式调用 httpfile.close()。}

\subsection{全栈支持 django}

在上面的例子里, 我们已经将 django 架设在 eurasia 上运行了。
这使得 django 能够工作在 stackless 模式下, 每个请求都使用 tasklet 处理。
此时 django 能够响应很高的并发, 但 django 的磁盘和数据库 IO 将不能正常工作。
如果你有很高的并发需求, 并且不带磁盘 IO, 这是最好的选择。

完全 (全栈) 支持 django 包括 IO 在内的所有功能, 需要使其工作在传统线程模式下,
这需要用到线程池 (shelve2.Pool)。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
# django 全栈服务器启动脚本

import sys, os
sys.path.insert(0, '/PATH/TO/DJANGO/PROJECTS')
os.environ['DJANGO_SETTINGS_MODULE'] = 'SITE.settings'

from eurasia.cgietc import wsgi
from eurasia.shelve2 import Pool
from eurasia import config, mainloop
from django.core.handlers.wsgi import WSGIHandler

threading = Pool(32) # 线程池预启动 32 个线程

config(handler=threading(wsgi(WSGIHandler())), port=8080)
mainloop() 
\end{verbatim}

\section{TCP 服务器设计}

除了 http 服务器之外, eurasia 还可以用于设计 tcp 服务器。

下面是使用 eurasia 编写的一个简单的 echo 服务器。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia import config, mainloop

# echo 服务器
def handler(tcpfile):
	while True:
		data = tcpfile.readline(1024) # 从 socket 读取
		if data.strip() == 'quit':
			tcpfile.close()
			break
		else:
			tcpfile.write(data)   # 输出到 socket

config(tcphandler=handler, port=8080) # 这里使用 tcphandler
mainloop()
\end{verbatim}

可以看到, 基本上使用 eurasia 编写 tcp 服务器与编写 http 服务器区别不大。

这里 tcp 服务器的 tcpfile 并不对 socket 中的内容作多余的处理,
你可以得到完整的 socket 输入, 并原样输出到 socket 客户端。

另一个区别在于 tcp 在 config 函数中是使用 tcphandler 指定的而不是原来的 handler。

在 eurasia 中编写 tcp 服务器甚至要比编写一个 http 服务器更简单,
eurasia 的 tcp 接口很少, 只涉及到 tcpfile 的输入输出。下面是在 tcp 服务器中可用的 tcpfile 属性。

\begin{tableii}{c|l}{}{ 属性 }{ 信息 }
\lineii{ tcpfile.pid            }{ 当前 socket 连接的 fileno }
\lineii{ tcpfile.read(size)     }{ 从 socket 中读取指定大小的内容, 默认读取全部内容 }
\lineii{ tcpfile.readline(size) }{ 行读取 }
\lineii{ tcpfile.write(data)    }{ 写 socket }
\lineii{ tcpfile.close()        }{ 关闭 socket 连接 }
\end{tableii}

这样, 基于 eurasia 的 tcp 接口, 我们现在可以方便地设计出 ftp、smtp 等基于 tcp 协议的服务器。

\section{使用标准模板}

eurasia.template 模块是 Mako 模板的一个简化版, 带有大部分 Mako 模板的功能, 和相同的标签语法。
同时 eurasia 标准模板也是编译型的模板。

template 包涵以下标签语法。

\begin{itemize}

\item \strong{表达式替换}

\begin{verbatim}
this is x: ${x}
\end{verbatim}

进一步, 取值表达式可以嵌入 python 代码, 并替换为 python 表达式的运算结果:

\begin{verbatim}
${int(a) + int(b)}
\end{verbatim}

\item \strong{控制结构}

我们可以在模板中使用条件表达式及叠代循环表达式。

这里是条件表达式:

\begin{verbatim}
%if x==1:
	x is ${x}
%elif x==2:
	x is ${x}
%else:
	x is ${x}
%endif
\end{verbatim}

这里是循环:

\begin{verbatim}
%for a in ['one', 'two', 'three', 'four', 'five']:
	%if a[0] == 't':
		its two or three
	%elif a[0] == 'f':
		four/five
	%else:
		one
	%endif
%endfor
\end{verbatim}

\item \strong{python 代码}

可以在模板中运行 python 代码:

\begin{verbatim}
<% # 这里是 Python 代码
a = 1
b = 2
%>

测试一下 a + b : ${a + b}
\end{verbatim}

\note{第一行 python 代码的缩进会被忽略, 
所以请避免使用 def、class、if、for 等依赖缩进的 python 代码。
函数中的 python 代码块在当前函数中可见, 函数外的代码块在当前模板中全局可见。}

\item \strong{函数标签 <\%def name="..." \%>}

函数是模板中最基本的调用单位, 类似于 Python 中的函数:

\begin{verbatim}
<%def name="myfunc(x)">
    this is myfunc, x is ${x}
</%def>

调用: ${myfunc(7)}
\end{verbatim}

\item \strong{<\%call expr="..."\%> 标签}

call 标签用于调用 <\%def\%> 标签，可传递额外的内嵌内容。稍后介绍。

\end{itemize}

\subsection{使用模板}

为了演示 eurasia 标准模板的用法, 这里将建立一个模板的应用范例。

在这个例子里, 我们首先将编写一个字符串模板, 带有 test1 和 test2 两个函数。
然后使用 template 模块中的 Template 函数将字符串模板编译成可执行的 python 模块,
并对模板中的 test1、test2 函数进行调用:

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia.template import Template

s = '''\                  # 字符串模板
<%def name="test1(a)">
	test1, a is ${a}
</%def>

<%def name="test2(b)">
	test2, b is ${b}
</%def>'''

tmpl = Template(s)        # 将字符串模板编译成可执行的 Python 模块

print tmpl.test1('hello') # 调用模板中的 "test1" 函数
print tmpl.test2('world') # 调用模板中的 "test2" 函数
\end{verbatim}

结果是:

\begin{verbatim}
test1, a is hello
test2, b is world
\end{verbatim}

<\%def> 标签定义了模板的调用接口, 在这个例子中, 我们定义了 test1 和 test2 两个调用接口。

\note{我们已经确认 CPython 中存在的一个 bug 将导致 Template(s).test1('hello') 这种写法报错。
因此, 在 CPython 中需要分开来写成 tmpl = Template(s); tmpl.test1('hello')。
也就是上面这个例子中的写法。非 C 实现的 Python 没有这个问题。}

\subsection{一个更为复杂的例子}

本例中, 将用到大量模板常用语法。包括有条件判断, 循环等:

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia.template import Template

s = '''\
<%def name="main(lst)">
--------------------------------------------
<% # python 代码块示范
class Foo:
	def test(self):
		return 'from foo.test()'

foo = Foo()

# 在 python 代码块中可以使用 write 函数进行内容输出
write('\\n' + foo.test())

%>

${Foo().test()}

--------------------------------------------
# 循环判断示范
%for i in lst:
	%if i == 1:
		${1+2}
	%elif i == 2:
		${'hello' + 'world'}
	%else:
		${i}
	%endif
%endfor

--------------------------------------------
# 嵌入式函数示范, 只在 main 函数中可见
<%def name="bar()">
	this is bar
</%def>
${bar()}

--------------------------------------------
</%def>'''

tmpl = Template(s)
print tmpl.main([1, 2, 3, 4, 5])
\end{verbatim}

\subsection{使用 <\%call> 标签定义宏}

<\%call> 标签提供了一种较为高级的功能, 也就是模板宏:

\begin{verbatim}
<%def name="macros(a, b)">
${caller.slot1(a)}
${caller.slot2(b)}
</%def>

----------------------------

<%def name="main()">
<%call expr="macros(1, 2)">
  <%def name="slot1(a)">
    slot1, ${a}
  </%def>

  <%def name="slot2(b)">
    slot1, ${b}
  </%def>
</%call>
</%def>
\end{verbatim}

这里首先使用 <\%def> 定义了一个名为 macros 的模板宏。

然后在 main 函数中使用 <\%call> 调用 macros, 其中定义了 slot1 和 slot2,
在 macros 中可以通过 caller 取出。

该模板的入口函数是 main()。调用结果是:

\begin{verbatim}
slot1, 1
slot2, 2
\end{verbatim}

\note{本例中在同一个模板中同时包涵了模板宏和宏调用, 通常情况下, 我们会将模板宏保存入外部文件。}

\note{call 只能在函数中 (<\%def>) 调用。如果把 call 放置在模板顶层, 模板在编译时会忽略 call 调用。}

\subsection{缓存编译结果}

eurasia 标准模板是一种编译型模板, 字符串模板经过 Template 函数可以编译成可执行的 python 模块。

我们也可以仅仅把字符串模板转换成 python 源代码, 在文件中保存起来,
这样可以在下次使用时直接 import 进来, 省去了编译过程, 尽管这花不了多少时间。

这里我们将用到 template 的 compile 工具。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia.template import compile

s = '''\
<%def name="main()">
	hello world!
</%def>

# 得到 python 源码
code = compile(s)

# 保存为 python 模块文件
fd = open('cache.py', w)
fd.write(code)
fd.close()

# 以模块方式导入模板
import cache
print cache.main()
\end{verbatim}

\subsection{<\%namespace> 标签}

Euraisa 标准模板中没有 Mako 中 <\%namespace> 标签的等价物, 这个功能相当于 Python 中的 import。
<\%namespace> 需要一系列关于导入路径的配置, 这比较复杂, 因此 Eurasia 换了一种方式。

Template 函数第二个参数可以设定模板中可见的环境。你可以在这里预先导入一些模板中用得到的东西, 比如宏。

这个例子在讲解 <\%call> 时出现过, 这里把宏和调用部分拆离开来。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia.template import Template
s1 = '''\ # 定义模板宏
<%def name="macros(a, b)">
${caller.slot1(a)}
${caller.slot2(b)}
</%def>'''

s2 = '''\ # 调用模板宏
<%def name="main()">
<%call expr="macros(1, 2)">
  <%def name="slot1(a)">
    slot1, ${a}
  </%def>

  <%def name="slot2(b)">
    slot1, ${b}
  </%def>
</%call>
</%def>'''

tmpl1 = Template(s1)

# 使用 env 参数指定模板环境
tmpl2 = Template(s2, env={'macros': tmpl1.macros})

tmpl2.main()
\end{verbatim}

\section{使用对象数据库}

eurasia 带有一种极其轻便的嵌入式对象数据库 shelve2。
如果用户具有 zodb 或者 durus 等对象数据库的使用经验,
会有助于理解 shelve2 的使用方法和工作方式, 但是这不是必须的, 因为 shelve2.py 要简单得多。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia.shelve2 import open

# 打开名为 test.fs 的数据库文件
d = open('test.fs')
d['foo'] = {'a': 1, 'b': 2, 'c': 3}

#d.sync() # 手动保存
d.close() # 数据库关闭时会自动保存

# 再次打开
d2 = open('test.fs')
print d2['foo']
\end{verbatim}

\subsection{对象数据库}

在对象数据库中, python 对象可以直接保存在数据库中, 而无须像关系数据库那样经过 "对象关系映射"。

下面, 我们将定义一个 MyObject 对象并存入 shelve2 数据库。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from eurasia.shelve2 import open

class MyObject:
        def __init__(self):
                self.a = None

        def print_a(self):
                print self.a

d = open('test.fs')
d['myobj'] = MyObject()
d['myobj'].a = 'hello world!'
d.close()

# 注意, 在打开数据库时, 对象定义 class MyObject 必须存在
# 否则会得到 AttributeError 的异常
d2 = open('test.fs')
d2['myobj'].print_a()
\end{verbatim}

\subsection{使用 Persistent 创建持久对象结点}

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from shelve2 import open
from shelve2 import Persistent

d = open('test.fs')
obj = Persistent() # 创建持久对象结点,
obj.a = 'hello'
obj.b = 'world'
d['obj'] = obj
d.close()

d2 = open('test.fs')
obj = d2['obj']
print '%s %s!' %(obj.a, obj.b)
\end{verbatim}

在对象数据库中, Persistent 不同于一般的 Object。
他们的区别在于在 Persistent 对象和普通 Object 同时作为父对象的属性时,
当父对象被展开、取出对象数据库时, 作为父对象属性的常规 Object 也会被自动展开读取到内存中;
而 Persistent 对象在父对象展开时仅以指针形式存在, 不会被展开到内存中。
所以 Persistent 对象更节省内存并节约 IO, 这使得 Persistent 对象非常适合作为数据节点。

\subsection{定制 Persistent 对象}

自定义 Persistent 对象需从 Persistent 类继承。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from shelve2 import open
from shelve2 import Persistent

class Foo(Persistent):
	def __init__(self, a, b):
		self.a = a
		self.b = b

	def print_ab(self):
		print '%s %s!' %(self.a, self.b)

d = open('test.fs')
d['foo'] = Foo('hello', 'world')
d.close()

d2 = open('test.fs')
d2['foo'].print_ab()
\end{verbatim}

\subsection{使用 BTree 创建大容量数据结点}

BTree 对象可以用以保存数以千万计的对象, 同时被保存的这些对象是按照 Key 排序的, 你可以以任何方式定位、遍历这些内容。

\begin{verbatim}
#!/usr/bin/python2.5
#-*- coding: utf-8 -*-
from shelve2 import open
from shelve2 import BTree

d = open('test.fs')
tr = BTree()
for i in xrange(1000):
	tr[i] = `i`

d['tr'] = tr
d.close()

d2 = open('test.fs')
print d2['tr']
\end{verbatim}

BTree 除了支持所有类似于 dict 的方法外, 还支持各种遍历方法:

\begin{tableii}{c|l}{}{ 属性 }{ 说明 }
\lineii{ BTree.iter()      }{ 正向遍历 }
\lineii{ reversed(BTree)   }{ 反向遍历 }
\lineii{ BTree.iteritems() }{ 正向遍历返回键、值 }
\lineii{ BTree.items_backward() }{ 反向遍历返回键、值 }
\lineii{ BTree.items_from(key, closed=True) }{ 正向遍历返回键、值 }
\lineii{ BTree.items_backward_from(key, closed=True) }{ 反向遍历返回键、值 }
\lineii{ BTree.items_range(start, end, closed_start=True, closed_end=False) }{ 遍历区域返回键、值 }
\end{tableii}

BTree 对象可以以和 Persistent 对象类似的方法派生。

shelve2 对象数据库的根对象就是一个 BTree 对象。 

\subsection{Persistent 和 BTree}

在 zope/plone 中, 不仅文件是 Persistent 对象, 连文件夹也是 Persistent 对象,
此时一个文件就是 Persistent 对象的一个属性。
只有在文件夹过大的时候我们才会使用从 BTree 派生出来的 PloneLargeFolder。
所以从本质上说, Persistent 和 BTree 都是容器对象,
他们的区别在于 Persistent 对象的内容在数据库里是连续存储,
而在内存中则变成 Hash Table, 在展开 Persistent 对象时其所有的属性也会随之一次性全部展开;
而 BTree 对象无论在数据库中还是内存中都是以平衡树的方式存储的,
读取 BTree 对象时 BTree 对象中的内容不会一次完全展开, 而是按需要分批展开。

所以在需要频繁读取容器内容时、并且容器中放置的内容并不多的情况下 Persistent 会快于 BTree。
而当保存的内容很多时 BTree 要更快。但是我们往往很难知道某个容器将来到底会扩充到何种规模,
所以可以在任何时候都使用 BTree 作为容器, 在性能上这其实并无大碍。

\section{配置文件支持}

eurasia.pyetc 模块为配置文件的读取提供了支持。eurasia 认为使用 python 语法来编写配置文件是个好主意。

pyetc 中的 load(filename, env={}) 函数能读取指定的 python 源码文件 (文件后缀并不需要一定是 ".py"), 并设定配置文件中的可见环境 env。load 函数能将指定源码文件转换成 python 模块并返回。

我们首先编写一个名为 \file{httpd.conf} 的配置文件:

\begin{verbatim}
Server(controller='Products.default.controller'
	port=8080)

user = 'nobody'
\end{verbatim}

下面是对于这个配置文件的解析:

\begin{verbatim}
from eurasia import pyetc
config = {}
def Server(**args):
	config.update(args)

mod = pyetc.load('httpd.conf', env={Server:Server})
print 'Server:', config
print 'user', mod.user
\end{verbatim}

\note{pyetc 模块可以被用于导入执行任意指定路径和文件后缀的 python 模块}

\section{启动为 daemon}

以 daemon 方式运行程序最简单的方法是使用 utility 模块的 daemonize 方法:

\begin{verbatim}
from eurasia import utility
utility.dummy()
utility.daemonize(PROGRAM, ARG1, ARG2, ...)
\end{verbatim}

daemon 模块略复杂一些:

\begin{verbatim}
from socket import error as SocketError
from eurasia import Daemon, ServerProxy,Fault

# 启动 daemon 管理器, start daemon
from os import fork
daemon = Daemon( # daemon 管理器
        address='/PATH/TO/pidfile',
        program=PROGRAM,
        verbose=False)

pid = fork()
if pid != 0:
        daemon(ARG1, ARG2, ...) # 启动 daemon 程序

# 获取 daemon 状态
daemon = ServerProxy('/PATH/TO/pidfile')
try:
        status = daemon.status() # 取到 'running' 或 'stopped'
except SocketError:
        raise # daemon 管理器未启动, 启动相应处理

# 停止 daemon
# 获取 daemon 状态
daemon = ServerProxy('/PATH/TO/pidfile')
try:
        status = daemon.stop() # 取到 'running' 或 'stopped'
except SocketError:
        raise # daemon 管理器未启动, 启动相应处理
except Fault:
        raise # 停止失败, 启动相应处理
\end{verbatim}


\chapter{API 文档}

\section{eurasia}

\begin{itemize}

\item \strong{config(**args)}

config() 参数可以接受如下参数

\begin{tableii}{c|l}{}{ 参数 }{ 说明 }
\lineii{ handler     }{ 指定一个服务器 handler, 如果指定地址, 则为  http 服务器, 否则即为 fcgi 服务器 }
\lineii{ controller  }{ 同 handler }
\lineii{ httphandler }{ 指定一个 http 服务器的 handler }
\lineii{ fcgihandler }{ 指定一个 fcgi 服务器的 handler }
\lineii{ tcphandler  }{ 指定一个 tcp 服务器的 handler }
\lineii{ application }{ 指定一个 wsgi 服务器的 aplication }
\lineii{ app  }{ 同 application }
\lineii{ wsgi }{ 同 application }
\lineii{ port }{ 指定服务器 port, 比如 8080 }
\lineii{ bind }{ 指定多个服务器地址, 比如 '192.168.0.1:8080, 192.168.0.2:80' }
\lineii{ fcgi }{ True 或者 False, 指定是否以 fcgi 模式启动 }
\lineii{ ssl_certificate }{ 指定 ssl cert 文件 }
\lineii{ ssl_private_key }{ 指定 ssl key 文件 }
\end{tableii}

\note{只能指定一个 handler (包括 wsgi application), port 和 bind 不能同时指定}

\item \strong{mainloop(cpus=False)}

启动由 config() 设置的所有服务器, 应用全局 verbose、uid、procname 设置。

如果 cpus 参数默认 False, 使用单进程。如果 cpus 为 True 则启动本机 cpu (及 cpu 核心) 数量的进程。
你也可以直接把 cpus 设定为某一整数, 手工指定进程数量。

\note{在多进程模式下, 全局变量不是进程间共享的, 你需要使用诸如 dbus 等进程间通信手段。}

\note{fcgi 服务器会忽略 cpus 设定, 总是使用单进程。
事实上, 诸如 apache 这类 http 服务器会启动多个 fcgi 服务器进程, 亦即 fcgi 服务器总是多进程的。}

\item \strong{WSGIServer(application, port=None, bind=None, bindAddress=None, verbose=True)}

返回一个 WSGIServer 对象。

你可以通过 port、bind、bindAddress 之一指定端口、多个地址、单个地址。
port、bind 的设置与 config() 函数一样, bindAddress 参数是为了与 flup.WSGIServer 相兼容。

你可以通过 WSGIServer 对象的 run(cpus=False) 方法或者 server_forever(cpus=False) 方法来启动服务器。

\item \strong{verbose}

如果设置了 eurasia.verbose, 并且取值为 False,
那么通过 eurasia.mainloop() 启动服务器, 会禁止一切调试输出。

\note{该项设置对 eurasia.web.mainloop()、eurasia.fcgi.mainloop() 无效}

\item \strong{uid}

如果设置了 eurasia.uid, 那么通过 eurasia.mainloop() 启动服务器,
会使用指定用户。你可以直接指定用户名。

\note{使 uid 生效需要 root 权限}

\note{该项设置对 eurasia.web.mainloop()、eurasia.fcgi.mainloop() 无效}

\item \strong{user}

与 eurasia.uid 相同, 但是你不能同时设置 eurasia.uid 和 eurasia.user。

\item \strong{procname}

如果设置了 eurasia.procname, 那么通过  eurasia.mainloop() 启动服务器,
会使用指定的进程名。

\note{该项设置对 eurasia.web.mainloop()、eurasia.fcgi.mainloop() 无效}

\item \strong{libc}

如果设置了 eurasia.procname, 那么通过  eurasia.mainloop() 启动服务器, 会使用指定的进程名。
这需要用到 libc, 大部分情况下 eurasia 的 libc 默认值 "/lib/libc.so.6" 都可以工作,
但是在少数情况诸如嵌入式系统上, 你需要设置 libc 为正确的位置, 比如 "/lib/libuClibc-0.9.27.so"。

\note{如果没有设置 eurasia.procname, 该项设置会被忽略}

\note{该项设置对 eurasia.web.mainloop()、eurasia.fcgi.mainloop() 无效}

\end{itemize}

\section{eurasia.socket2}

\begin{itemize}

\item \strong{config(**args)}

config() 参数可以接受如下参数

\begin{tableii}{c|l}{}{ 参数 }{ 说明 }
\lineii{ handler     }{ 指定一个 tcp 服务器 handler }
\lineii{ controller  }{ 同 handler }
\lineii{ tcphandler  }{ 同 handler }
\lineii{ port }{ 指定服务器 port, 比如 8080 }
\lineii{ bind }{ 指定多个服务器地址, 比如 '192.168.0.1:8080, 192.168.0.2:80' }
\lineii{ ssl_certificate }{ 指定 ssl cert 文件 }
\lineii{ ssl_private_key }{ 指定 ssl key 文件 }
\end{tableii}

\note{只能指定一个 handler, port 和 bind 不能同时指定}

\item \strong{mainloop(cpus=False)}

启动由 socket2.config() 设置的所有服务器。

如果 cpus 参数默认 False, 使用单进程。如果 cpus 为 True 则启动本机 cpu (及 cpu 核心) 数量的进程。
你也可以直接把 cpus 设定为某一整数, 手工指定进程数量。

\note{在多进程模式下, 全局变量不是进程间共享的, 你需要使用进程间通信。}

\item \strong{SocketFile 对象}

SocketFile 对象是由 TcpServer 生成并传递给 tcphandler 的。

SocketFile 对象的属性如下。

\begin{tableii}{c|l}{}{ 属性/方法 }{ 说明 }
\lineii{ socket   }{ 原始 socket }
\lineii{ pid      }{ socket 的 fileno }
\lineii{ address  }{ socket 地址, 通常用于获取客户端地址, 取值为 (ip, port) }
\lineii{ fileno() }{ 取得 socket 的 fileno }
\lineii{ read(size=-1)     }{ 从  socket 读取指定大小的数据, 默认读取所有 }
\lineii{ readline(size=-1) }{ 行读取 }
\lineii{ write(data) }{ 向 socket 写入数据 }
\lineii{ close() }{ 关闭 socket }
\end{tableii}

\note{在 read()/readline()/write()/close() 等 IO 操作中如果客户端已经断开连接则会抛出 IOError}

\item \strong{TcpHandler(handler)}

声明 handler 为 tcphandler。

\begin{verbatim}
@TcpHandler
def handler(sockfile):
	...
\end{verbatim}

\item \strong{TcpServerSocket(address)}

返回一个绑定到  address 的 socket。

\item \strong{TcpServer(sock, handler)}

创建一个绑定到 sock 使用 handler 的 tcp 服务器, 可以通过 mainloop() 启动该服务器。

下面是一个完整的例子。

\begin{verbatim}
from eurasia.socket2 import TcpServer, TcpServerSocket, TcpHandler
def handler(sockfile):
	sockfile.write('hello world!')
	sockfile.close()

# config(tcphandler=handler, port=8080)
TcpServer(TcpServerSocket('0.0.0.0', 8080), TcpHandler(handler))

mainloop()
\end{verbatim}

\end{itemize}

\section{eurasia.web}

\begin{itemize}

\item \strong{config(**args)}

config() 参数可以接受如下参数

\begin{tableii}{c|l}{}{ 参数 }{ 说明 }
\lineii{ handler     }{ 指定一个 http 服务器 handler }
\lineii{ controller  }{ 同 handler }
\lineii{ httphandler }{ 同 handler }
\lineii{ tcphandler  }{ 指定一个 tcp 服务器 handler }
\lineii{ application }{ 指定一个 wsgi 服务器的 aplication }
\lineii{ app  }{ 同 application }
\lineii{ wsgi }{ 同 application }
\lineii{ port }{ 指定服务器 port, 比如 8080 }
\lineii{ bind }{ 指定多个服务器地址, 比如 '192.168.0.1:8080, 192.168.0.2:80' }
\lineii{ ssl_certificate }{ 指定 ssl cert 文件 }
\lineii{ ssl_private_key }{ 指定 ssl key 文件 }
\end{tableii}

\note{只能指定一个 handler, port 和 bind 不能同时指定}

\item \strong{mainloop(cpus=False)}

启动由 web.config() 设置的所有服务器。

如果 cpus 参数默认 False, 使用单进程。如果 cpus 为 True 则启动本机 cpu (及 cpu 核心) 数量的进程。
你也可以直接把 cpus 设定为某一整数, 手工指定进程数量。

\note{在多进程模式下, 全局变量不是进程间共享的, 你需要使用进程间通信。}

\item \strong{HttpFile 对象}

HttpFile 对象是由 TcpServer 生成并传递给 httphandler 的。

HttpFile 对象的属性如下。

\begin{tableii}{c|l}{}{ 属性/方法 }{ 说明 }
\lineii{ sockfile   }{ 原始 socket 的 SocketFile 封装, 由服务器传递给 httphandler }
\lineii{ pid        }{ sockfile 的 fileno }
\lineii{ address    }{ socket 地址, 通常用于获取客户端地址, 取值为 (ip, port) }
\lineii{ uid        }{ 读取和指定用户的持久 id }
\lineii{ environ    }{ 一个 dict, 存放 cgi 环境变量, 也是 WSGI 环境变量 }
\lineii{ request_uri  }{ 读取和设置 REQUEST_URI 环境变量 (存放于 httpfile.environ), 会影响到 path_info、script_name、query_string }
\lineii{ path_info    }{ 读取和设置 PATH_INFO 环境变量 (存放于 httpfile.environ), 会影响到 path_info、query_string }
\lineii{ script_name  }{ 读取和设置 SCRIPT_NAME 环境变量, 会影响到 request_uri }
\lineii{ query_string }{ 读取和设置 QUERY_STRING 环境变量, 会影响到 request_uri }
\lineii{ __getitem__(key) }{ 读取请求的头部 }
\lineii{ __setitem__(key) }{ 设置响应的头部 }
\lineii{ fileno()   }{ 取得 socket 的 fileno }
\lineii{ read(size=-1)     }{ 从  http 请求读取指定大小的数据, 默认读取所有 }
\lineii{ readline(size=-1) }{ 行读取 }
\lineii{ write(data)       }{ 向 http 客户端写入数据 }
\lineii{ close() }{ 结束本次请求 }
\lineii{ begin() }{ 发送 http 头, 并进入长连接流模式 }
\lineii{ wbegin(data) }{ 发送 http 头和 data, 并进入流模式, 相当于 begin() + write(data) }
\end{tableii}

\note{在 read()/readline()/write()/close() 等 IO 操作中如果客户端已经断开连接则会抛出 IOError}

\item \strong{HttpHandler(handler, server_name='localhost', server_port='80')}

声明 handler 为 httphandler。

\begin{verbatim}
@HttpHandler
def handler(httpfile):
	...
\end{verbatim}

\item \strong{TcpServerSocket(address)}

返回一个绑定到  address 的 socket。

\item \strong{TcpServer(sock, handler)}

创建一个绑定到 sock 使用 handler 的 tcp 服务器, 可以通过 mainloop() 启动该服务器。

下面是一个完整的例子。

\begin{verbatim}
from eurasia.web import TcpServer, TcpServerSocket, HttpHandler
def handler(httpfile):
	httpfile.write('hello world!')
	httpfile.close()

# config(httphandler=handler, port=8080)
TcpServer(TcpServerSocket('0.0.0.0', 8080),
          HttpHandler(handler, server_name='localhost', server_port='80'))

mainloop()
\end{verbatim}

\item \strong{Form(httpfile, max_size=1048576)}

表单处理, 见 eurasia.cgietc.SimpleUpload

\item \strong{SimpleUpload(httpfile)}

上传文件处理, 见 eurasia.cgietc.SimpleUpload

\item \strong{Browser(httpfile)}

远程 js 调用, 见 eurasia.cgietc.Browser

\item \strong{json(object)}

将 python 基础对象转换成 json 报文。仅支持 dict、list、int、long、float、basestring。

\note{非 ascii 字符必须是 unicode 而非 str}

\item \strong{wsgi(application)}

将 WSGI application 转换成 handler。

\end{itemize}

\section{eurasia.fcgi}

eurasia.web 的 fastcgi 克隆

\begin{itemize}

\item \strong{config(**args)}

config() 参数可以接受如下参数

\begin{tableii}{c|l}{}{ 参数 }{ 说明 }
\lineii{ handler     }{ 指定一个 fcgi 服务器 handler }
\lineii{ controller  }{ 同 handler }
\lineii{ fcgihandler }{ 同 handler }
\lineii{ application }{ 指定一个 wsgi 服务器的 aplication }
\lineii{ app  }{ 同 application }
\lineii{ wsgi }{ 同 application }
\lineii{ port }{ 指定服务器 port, 比如 8080 }
\lineii{ bind }{ 指定多个服务器地址, 比如 '192.168.0.1:8080, 192.168.0.2:80' }
\lineii{ ssl_certificate }{ 指定 ssl cert 文件 }
\lineii{ ssl_private_key }{ 指定 ssl key 文件 }
\end{tableii}

\note{只能指定一个 handler, port 和 bind 不能同时指定}

\item \strong{mainloop(cpus=False)}

启动由 fcgi.config() 设置的所有服务器。

如果 cpus 参数默认 False, 使用单进程。如果 cpus 为 True 则启动本机 cpu (及 cpu 核心) 数量的进程。
你也可以直接把 cpus 设定为某一整数, 手工指定进程数量。

\note{在多进程模式下, 全局变量不是进程间共享的, 你需要使用进程间通信。}

\item \strong{FcgiFile 对象}

FcgiFile 对象是由 TcpServer 生成并传递给 fcgihandler 的。

FcgiFile 的属性与 HttpFile 相同。

\note{FcgiFile 可以通过调用 begin() 进入长连接流模式, 但是会阻塞掉该 fcgi 进程, 因此不推荐使用}

\item \strong{FcgiHandler(handler)}

声明 handler 为 fcgihandler。

\begin{verbatim}
@FcgiHandler
def handler(httpfile):
	...
\end{verbatim}

\item \strong{FcgiServerSocket()}

返回一个 fcgi 服务器 socket。

\item \strong{TcpServer(sock, handler)}

创建一个绑定到 sock 使用 handler 的 tcp 服务器, 可以通过 mainloop() 启动该服务器。

下面是一个完整的例子。

\begin{verbatim}
from eurasia.fcgi import TcpServer, FcgiServerSocket, FcgiHandler
def handler(httpfile):
	httpfile.write('hello world!')
	httpfile.close()

# config(fcgihandler=handler)
TcpServer(FcgiServerSocket(), FcgiHandler(handler))

mainloop()
\end{verbatim}

\item \strong{Form(httpfile, max_size=1048576)}

表单处理, 见 eurasia.cgietc.SimpleUpload

\item \strong{SimpleUpload(httpfile)}

上传文件处理, 见 eurasia.cgietc.SimpleUpload

\item \strong{Browser(httpfile)}

远程 js 调用, 见 eurasia.cgietc.Browser

\item \strong{json(object)}

将 python 基础对象转换成 json 报文。仅支持 dict、list、int、long、float、basestring。

\note{非 ascii 字符必须是 unicode 而非 str}

\item \strong{wsgi(application)}

将 WSGI application 转换成 handler。

\end{itemize}

\section{eurasia.cgietc}

\begin{itemize}

\item \strong{Form(httpfile, max_size=1048576)}

Form 函数将用户提交的表单解析成一个 dict 对象。

你可以通过设定 max_size 来限制用户 POST 数据的大小, 如果用户提交的数据超过限制, Form 函数会抛出 IOError。

\note{如果用户提交了多个重名的表单变量, Form 会使用 list 来保存。比如 "?a=1\&a=2\&a=3\&b=4", form['a'] 的取值是 ['1', '2', '3'], 而 form['b'] 的取值是 '4'。}

\item \strong{SimpleUpload(httpfile)}

SimpleUpload 会返回一个上传文件的文件描述符 (fd), 我们可以从 fd.filename 和 fd.size 中取出文件名和文件大小。
其余的表单变量可以通过 fd[key] 取出。

\begin{tableii}{c|l}{}{ 属性 }{ 信息 }
\lineii{ SimpleUpload.size           }{ 上传文件的大小 }
\lineii{ SimpleUpload.filename       }{ 上传文件的文件名 }
\lineii{ SimpleUpload[key]           }{ 除了上传文件之外, 其余表单变量 }
\lineii{ SimpleUpload.read(size)     }{ 从上传文件中读取指定大小的内容, 默认读取全部内容 }
\lineii{ SimpleUpload.readline(size) }{ 行读取 }
\end{tableii}

\note{SimpleUpload 要求表单变量名 (这里是 'a') 必须小于文件控件的名称 (这里是 'z-file')。
因为对 SimpleUpload 组件来说, 文件组件的名字并不重要, 所以请尽量起成 "zzzzzzzz" 之类的名字,
以保证他的值是最大的。另外, SimpleUpload 组件只支持上传一个文件。
当需要提交多个文件时, 你可以使用多个 SimpleUpload 组件。}

\note{SimpleUpload 并不会真的把用户上传的文件建立在磁盘上, 也不会把用户的上传读取到内存中,
而是在调用 read() 时才会从 socket 中去获取。因此, 在没有读取完整个上传文件之前,
上传用户的浏览器将一直保持在 "正在上传" 的状态下。}

\note{http 协议并不是每次都确切地知道用户上传文件的大小,
SimpleUpload 能够猜测出用户上传内容的大小, 对于标准的浏览器和客户端这通常是准确的。
在极少数情况下 SimpleUpload.size 和 SimpleUpload.read 取出的内容大小并不匹配,
这时应以 SimpleUpload.read 取出的实际内容大小为准, 一般来说, 你可以认为这是一种恶意行为。}

\note{在向用户发送任何内容之前, 请先确保已经完全读取了上传文件, 否则会发生读写错误 (IOError)。}

\note{在读取文件时如果用户已经断开连接, read 会抛出 IOError。}

\item \strong{Browser(httpfile, domain=None)}

取得浏览器对象, 可以直接调用远程浏览器上的 javascript 函数。

我们可以在 Brwoser 对象创建时通过 domain 指定 js 域 (document.domain),
方便 javascript 的跨子域调用。

\note{如果不指定 domain, Browser 对象会指定 domain 为顶级域名,
如果当前域为 "www.example.com" 那么默认即为 "example.com"}

\item \strong{json(object)}

将 python 基础对象转换成 json 报文。仅支持 dict、list、int、long、float、basestring。

\note{非 ascii 字符必须是 unicode 而非 str}

\item \strong{wsgi(application)}

将 WSGI application 转换成 handler。

\end{itemize}

\section{eurasia.shelve2}

\begin{itemize}

\item \strong{open(filename, pool=None)}

返回数据库连接对象。如果指定了 pool 则使用指定的线程池 (Pool)。

数据库连接对象具有 BTree 所有属性和方法, 用于操作根对象。

\begin{tableii}{c|l}{}{ 属性 }{ 信息 }
\lineii{ sync() }{ 保存所有的修改 }
\lineii{ close() }{ 保存所有修改并关闭连接 }
\end{tableii}

\item \strong{Pool(n=16)}

返回线程池对象。使用指定的线程数量 (n)。

\item \strong{Persistent 类}

Persistent 定义了对象数据库的持久对象。
可以增加、删除、修改 Persitent 对象的属性, 在调用数据库的保存接口时这些修改会自动提交。

\item \strong{BTree 类}

BTree 定义了对象数据库的持久 btree (类似于 python 字典) 对象。

BTree 支持的方法有

\begin{tableii}{c|l}{}{ 属性 }{ 说明 }
\lineii{ BTree[key] }{ 取值  }
\lineii{ BTree[key] = value) }{ 设置 key 的值  }
\lineii{ BTree.get(key, default=None) }{ 取值, 如果  key 不存在返回  default }
\lineii{ BTree.update(dict) }{ 批量赋值 }
\lineii{ BTree.keys() }{ 返回所有键 }
\lineii{ BTree.values() }{ 返回所有值 }
\lineii{ BTree.items() }{ 以  [(key, value), ...] 形式返回所有键值对 }
\lineii{ BTree.iter() }{ 正向遍历  }
\lineii{ BTree.reversed() }{ 反向遍历 }
\lineii{ BTree.iteritems() }{ 正向遍历返回键、值 }
\lineii{ BTree.items_backward() }{ 反向遍历返回键、值 }
\lineii{ BTree.items_from(key, closed=True) }{ 正向遍历返回键、值 }
\lineii{ BTree.items_backward_from(key, closed=True) }{ 反向遍历返回键、值 }
\lineii{ BTree.items_range(start, end, closed_start=True, closed_end=False) }{ 遍历区域返回键、值 }
\end{tableii}

\end{itemize}

\section{eurasia.template}

\begin{itemize}

\item \strong{Template(text, env={})}

解析字符串 text, 返回一个模板对象。可以通过 env 设定模板中可见的环境变量。

\item \strong{compile(text)}

将字符串 text 编译为 python 源码。

\end{itemize}

\section{eurasia.pyetc}

\begin{itemize}

\item \strong{load(fullpath, env={}, module=Module)}

导入指定路径 fullpath 的 python 源码, 返回为 python 模块。

通过设定  env 参数可以指定在导入 python 文件时可见的环境变量。

通过 module 参数可以指定返回的模块类型。

\item \strong{reload(module)}

重新加载由 pyetc.load() 导入的模块。

\item \strong{unload(module)}

卸载由 pyetc.load() 导入的模块。

\end{itemize}

\section{eurasia.utility}

\begin{itemize}

\item \strong{cpu_count()}

得到 cpu 或者 cpu 核心数。

\item \strong{setuid(user)}

修改当前程序的身份。

\item \strong{setprocname(procname, libc=None)}

修改当前进程的进程名。如果系统 libc 的位置不在 '/lib/libc.so.6' 你需要手工指定。

\note{不支持 windows}

\item \strong{dummy()}

将 stdin、stdout 以及 stderr 重定向到 /dev/null, 禁止标准输入输出。通常用于后台服务器进程。

\note{dummy() 会关闭系统 socket 句柄, 这意味着如果要使用 mod_fcgid、mod_fastcgi 来启动服务器,
服务器 socket 从 fileno=0 的 socket 句柄中取出, 必须在调用 config() 以后再执行 dummy(),
否则将取不到 apache 提供的 socket。 }

\item \strong{daemonize(program, *args)}

在后台执行 program, 使用指定的命令行参数 args。

调用 daemonize 以后, 本进程将结束。

\end{itemize}

\section{eurasia.daemon}

\note{eurasia.daemon 模块仅限于 unix/linux 系统。}

\note{eurasia.daemon 模块目前仅在 python2.5 中测试过, python2.6 兼容性未知。}

\begin{itemize}

\item \strong{Daemon 类}

Daemon 接受如下初始化参数

\begin{tableii}{c|l}{}{ 参数 }{ 说明 }
\lineii{ program }{ 在后台运行的程序, 这个参数是必须的 }
\lineii{ address }{ pid 文件名 (也可以指定一个 socket 地址) }
\lineii{ verbose }{ 如果设置为 False 则禁止调试 }
\lineii{ stdout  }{ 在禁止调试的情况下, 指定 stdout 重定向某个文件句柄 }
\lineii{ stderr  }{ 在禁止调试的情况下, 指定 stderr 重定向某个文件句柄 }
\lineii{ directory }{ 指定进程工作目录 }
\lineii{ umask }{ 设置 umask }
\end{tableii}

Daemon 提供的方法

\begin{tableii}{c|l}{}{ 属性/方法 }{ 说明 }
\lineii{ __call__(*args) }{ 在后台启动 program, 使用命令行参数 args }
\lineii{ status() }{ 返回指定进程的运行状态, 'stopped' 或者 'running' }
\lineii{ stop() }{ 结束后台进程 program, 如果失败会抛出 daemon.error }
\lineii{register_function(func, name)}{ 因为 Daemon 是一个 xmlrpc 服务器, 你可以通过这个函数向 xmlrpc 服务器注册更多的方法 }
\end{tableii}

调用 Daemon 对象的  __call__ 方法以后, 将在指定的  address 建立一个 xmlrpc 服务。
xmlrpc 服务提供三个方法, 即 Daemon.status()、Daemon.stop()

\item \strong{ServerProxy 类}

通过 ServerProxy(address, **args) 可以得到指定地址 address 上的 xmlrpc 服务器。

args 参数类似 xmlrpclib.ServerProxy 的设定。

通过 ServerProxy 我们可以调用指定 address 上的 Daemon 服务器上的方法。具体请参见 xmlrpclib。

\item \strong{error 类 (异常)}

Daemon 相关的异常。

\end{itemize}



\end{document}
